/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.catalina.startup;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

/**
 * <p>Utility class for building class loaders for Catalina.  The factory
 * method requires the following parameters in order to build a new class
 * loader (with suitable defaults in all cases):</p>
 * <ul>
 * <li>A set of directories containing unpacked classes (and resources)
 * that should be included in the class loader's
 * repositories.</li>
 * <li>A set of directories containing classes and resources in JAR files.
 * Each readable JAR file discovered in these directories will be
 * added to the class loader's repositories.</li>
 * <li><code>ClassLoader</code> instance that should become the parent of
 * the new class loader.</li>
 * </ul>
 *
 * @author Craig R. McClanahan
 */

public final class ClassLoaderFactory {

	private static final Log log = LogFactory.getLog(ClassLoaderFactory.class);

	// --------------------------------------------------------- Public Methods

	/**
	 * Create and return a new class loader, based on the configuration
	 * defaults and the specified directory paths:
	 *
	 * @param unpacked Array of pathnames to unpacked directories that should
	 *                 be added to the repositories of the class loader, or <code>null</code>
	 *                 for no unpacked directories to be considered
	 * @param packed   Array of pathnames to directories containing JAR files
	 *                 that should be added to the repositories of the class loader,
	 *                 or <code>null</code> for no directories of JAR files to be considered
	 * @param parent   Parent class loader for the new class loader, or
	 *                 <code>null</code> for the system class loader.
	 * @throws Exception if an error occurs constructing the class loader
	 */
	public static ClassLoader createClassLoader(File unpacked[],
	                                            File packed[],
	                                            final ClassLoader parent)
			throws Exception {

		if (log.isDebugEnabled())
			log.debug("Creating new class loader");

		// Construct the "class path" for this class loader
		Set<URL> set = new LinkedHashSet<URL>();

		// Add unpacked directories
		if (unpacked != null) {
			for (int i = 0; i < unpacked.length; i++) {
				File file = unpacked[i];
				if (!file.exists() || !file.canRead())
					continue;
				file = new File(file.getCanonicalPath() + File.separator);
				URL url = file.toURI().toURL();
				if (log.isDebugEnabled())
					log.debug("  Including directory " + url);
				set.add(url);
			}
		}

		// Add packed directory JAR files
		if (packed != null) {
			for (int i = 0; i < packed.length; i++) {
				File directory = packed[i];
				if (!directory.isDirectory() || !directory.exists() ||
						!directory.canRead())
					continue;
				String filenames[] = directory.list();
				if (filenames == null) {
					continue;
				}
				for (int j = 0; j < filenames.length; j++) {
					String filename = filenames[j].toLowerCase(Locale.ENGLISH);
					if (!filename.endsWith(".jar"))
						continue;
					File file = new File(directory, filenames[j]);
					if (log.isDebugEnabled())
						log.debug("  Including jar file " + file.getAbsolutePath());
					URL url = file.toURI().toURL();
					set.add(url);
				}
			}
		}

		// Construct the class loader itself
		final URL[] array = set.toArray(new URL[set.size()]);
		return AccessController.doPrivileged(
				new PrivilegedAction<URLClassLoader>() {
					@Override
					public URLClassLoader run() {
						if (parent == null)
							return new URLClassLoader(array);
						else
							return new URLClassLoader(array, parent);
					}
				});
	}

	/**
	 * Create and return a new class loader, based on the configuration
	 * defaults and the specified directory paths:
	 *
	 * @param repositories List of class directories, jar files, jar directories
	 *                     or URLS that should be added to the repositories of
	 *                     the class loader.
	 * @param parent       Parent class loader for the new class loader, or
	 *                     <code>null</code> for the system class loader.
	 * @throws Exception if an error occurs constructing the class loader
	 */
	public static ClassLoader createClassLoader(List<Repository> repositories,
	                                            final ClassLoader parent)
			throws Exception {

		if (log.isDebugEnabled())
			log.debug("Creating new class loader");

		// Construct the "class path" for this class loader
		Set<URL> set = new LinkedHashSet<URL>();

		if (repositories != null) {
			for (Repository repository : repositories) {
				if (repository.getType() == RepositoryType.URL) {
					URL url = buildClassLoaderUrl(repository.getLocation());
					if (log.isDebugEnabled())
						log.debug("  Including URL " + url);
					set.add(url);
				} else if (repository.getType() == RepositoryType.DIR) {
					File directory = new File(repository.getLocation());
					directory = directory.getCanonicalFile();
					if (!validateFile(directory, RepositoryType.DIR)) {
						continue;
					}
					URL url = buildClassLoaderUrl(directory);
					if (log.isDebugEnabled())
						log.debug("  Including directory " + url);
					set.add(url);
				} else if (repository.getType() == RepositoryType.JAR) {
					File file = new File(repository.getLocation());
					file = file.getCanonicalFile();
					if (!validateFile(file, RepositoryType.JAR)) {
						continue;
					}
					URL url = buildClassLoaderUrl(file);
					if (log.isDebugEnabled())
						log.debug("  Including jar file " + url);
					set.add(url);
				} else if (repository.getType() == RepositoryType.GLOB) {
					File directory = new File(repository.getLocation());
					directory = directory.getCanonicalFile();
					if (!validateFile(directory, RepositoryType.GLOB)) {
						continue;
					}
					if (log.isDebugEnabled())
						log.debug("  Including directory glob "
								+ directory.getAbsolutePath());
					String filenames[] = directory.list();
					if (filenames == null) {
						continue;
					}
					for (int j = 0; j < filenames.length; j++) {
						String filename = filenames[j].toLowerCase(Locale.ENGLISH);
						if (!filename.endsWith(".jar"))
							continue;
						File file = new File(directory, filenames[j]);
						file = file.getCanonicalFile();
						if (!validateFile(file, RepositoryType.JAR)) {
							continue;
						}
						if (log.isDebugEnabled())
							log.debug("    Including glob jar file "
									+ file.getAbsolutePath());
						URL url = buildClassLoaderUrl(file);
						set.add(url);
					}
				}
			}
		}

		// Construct the class loader itself
		final URL[] array = set.toArray(new URL[set.size()]);
		if (log.isDebugEnabled())
			for (int i = 0; i < array.length; i++) {
				log.debug("  location " + i + " is " + array[i]);
			}

		return AccessController.doPrivileged(
				new PrivilegedAction<URLClassLoader>() {
					@Override
					public URLClassLoader run() {
						if (parent == null)
							return new URLClassLoader(array);
						else
							return new URLClassLoader(array, parent);
					}
				});
	}

	private static boolean validateFile(File file,
	                                    RepositoryType type) throws IOException {
		if (RepositoryType.DIR == type || RepositoryType.GLOB == type) {
			if (!file.exists() || !file.isDirectory() || !file.canRead()) {
				String msg = "Problem with directory [" + file +
						"], exists: [" + file.exists() +
						"], isDirectory: [" + file.isDirectory() +
						"], canRead: [" + file.canRead() + "]";

				File home = new File(Bootstrap.getCatalinaHome());
				home = home.getCanonicalFile();
				File base = new File(Bootstrap.getCatalinaBase());
				base = base.getCanonicalFile();
				File defaultValue = new File(base, "lib");

				// Existence of ${catalina.base}/lib directory is optional.
				// Hide the warning if Tomcat runs with separate catalina.home
				// and catalina.base and that directory is absent.
				if (!home.getPath().equals(base.getPath())
						&& file.getPath().equals(defaultValue.getPath())
						&& !file.exists()) {
					log.debug(msg);
				} else {
					log.warn(msg);
				}
				return false;
			}
		} else if (RepositoryType.JAR == type) {
			if (!file.exists() || !file.canRead()) {
				log.warn("Problem with JAR file [" + file +
						"], exists: [" + file.exists() +
						"], canRead: [" + file.canRead() + "]");
				return false;
			}
		}
		return true;
	}

	/*
	 * These two methods would ideally be in the utility class
	 * org.apache.tomcat.util.buf.UriUtil but that class is not visible until
	 * after the class loaders have been constructed.
	 */
	private static URL buildClassLoaderUrl(String urlString) throws MalformedURLException {
		// URLs passed to class loaders may point to directories that contain
		// JARs. If these URLs are used to construct URLs for resources in a JAR
		// the URL will be used as is. It is therefore necessary to ensure that
		// the sequence "!/" is not present in a class loader URL.
		String result = urlString.replaceAll("!/", "%21/");
		return new URL(result);
	}

	private static URL buildClassLoaderUrl(File file) throws MalformedURLException {
		// Could be a directory or a file
		String fileUrlString = file.toURI().toString();
		fileUrlString = fileUrlString.replaceAll("!/", "%21/");
		return new URL(fileUrlString);
	}

	public static enum RepositoryType {
		DIR,
		GLOB,
		JAR,
		URL
	}

	public static class Repository {
		private String location;
		private RepositoryType type;

		public Repository(String location, RepositoryType type) {
			this.location = location;
			this.type = type;
		}

		public String getLocation() {
			return location;
		}

		public RepositoryType getType() {
			return type;
		}
	}
}
